/*  This file is part of TextEditorLibrary - a library for the programmatic manipulation of text.

    TextEditorLibrary is free software: you can redistribute it and/or modify it under the terms
    of the GNU Lesser General Public License as published by the Free Software Foundation, either
    version 3 of the License, or (at your option) any later version.

    TextEditorLibrary is distributed in the hope that it will be useful, but WITHOUT ANY 
    WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
    PURPOSE.  See the GNU Lesser General Public License for more details at 
    <http://www.gnu.org/licenses/>.
*/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Diagnostics;
using System.Threading;

namespace TextEditorLibrary
{

/// <summary>Defines a buffer of text which can be manipulated programmatically.</summary>
    public partial class Text : IDisposable
    {
        internal const int tgtBlock = 4096;         // Aim for this size block - good for file read
        internal const int maxBlock = 4224;         // Make room for this size block - space for small inserts
        internal const int blockSize = maxBlock * sizeof(char) + 40; // Bytes per block - 40 was determined by experiment
        private long _readLength = 0;               // Accumulated length of Text as it is read

        private static int _IDs = 0;                // Number of Texts so far
        private static object _idLock = new object();   // Locking object to protect _IDs
        private int _ID;                            // Text ID for debug
        private bool _removeCR;                     // Whether carriage return characters have been removed on construction
        internal DataLoad _dataLoad;                // Data loading option

        private Thread _backThread;                 // Thread used by background load
        private object _backLock = new object();    // Locking object used by background load
        internal bool _backFinished;                // Flag for background load is finished (so last block really is)
        private bool _backQuit;                     // Flag for background load should stop
        private Exception _backError;               // Exception returned by background load

        private const int _demandMax = 100;         // Maximum number of disposable blocks in memory (<1MB)
        private Queue<TextBlock> _demandBlocks;     // Auto-truncated queue of blocks that we can dispose of
        private int _demandBOM;                     // Number of bytes in byte order mark
        private Thread _demandThread;               // Thread used by on-demand load
        private object _demandLock = new object();  // Locking object used by on-demand load
        private long _demandOffset;                 // File offset to required load block 
        private bool _demandReady;                  // On-demand thread has read that block
        private char[][] _demandBuffer;             // Data returned by on-demand thread (double buffer)
        private int _demandRead;                    // Number of chars read
        private int _demandWhich;                   // Buffer choice
        private bool _demandFinished;               // Flag for on-demand thread has stopped
        private bool _demandQuit;                   // Flag for on-demand thread should stop
        private Exception _demandError;             // Exception returned by on-demand thread

        internal TextBlockList _blocks;             // The text lives in a hand-made linked list of TextBlocks
        private string _filename;                   // Filename for writing out again
        private Encoding _encoding;                 // Character encoding for writing out again
        internal long _editLength = 0;               // Adjustment to number of characters in Text due to editing plus or minus

        internal Mark _cursor;                      // Mark for current position (also in list)

        private List<Mark> _marks = new List<Mark>();           // List of Marks registered by the caller to be moved with the text
        private const int maxInval = 100;                       // Number of invalidatable marks to track for conditional invalidation
        private Queue<Mark> _invalMarks = new Queue<Mark>();    // Auto-truncated queue of invalidatable (unregistered) marks

        internal List<Box> _boxes = new List<Box>();            // List of Boxes so we can tell when they get dirty

    /// <summary>Creates an empty Text.</summary>
        public Text() : this("")
        {
        }

    /// <summary>Creates a Text from a string. Carriage return characters are removed.</summary>
    /// <param name="text">string value to add to the Text.</param>
        public Text(string text) : this(text, true)
        {
        }

    /// <summary>Creates a Text from a string.</summary>
    /// <param name="text">string value to add to the Text.</param>
    /// <param name="removeCR">bool value to indicate whether carriage return characters should be removed.</param>
        public Text(string text, bool removeCR)
        {
            _ID = GetNextID();
            _blocks = new TextBlockList(this);
            _filename = "TelOutput.txt";
            _encoding = Encoding.ASCII;
            _removeCR = removeCR;
            _dataLoad = DataLoad.Immediate;
            char[] buffer = new char[tgtBlock];

            int length = text.Length;
            int pos = 0;

            while (pos<length)
            {
                int copy = Math.Min(length-pos,tgtBlock);
                text.CopyTo(pos, buffer, 0, copy);
                pos += tgtBlock;
                int clean = copy;
                if (removeCR) clean = CleanCarriageReturns(buffer, copy);
                if (clean>0) _blocks.AddLast(new TextBlock(buffer,clean));
                _readLength += clean;
            } 

            if (_blocks._count == 0) _blocks.AddLast(new TextBlock());

            _cursor = new Mark(this, false);
            _marks.Add(_cursor);
            Check();
        }

    /// <summary>Creates a Text from a text file. Carriage return characters are removed, and the whole
    /// file is read before returning.</summary>
    /// <param name="filename">string value containing the text file path.</param>
    /// <param name="encoding">Encoding value or null for default ASCII.</param>
    /// <exception cref="ArgumentException">The filename is an empty string.</exception>
    /// <exception cref="ArgumentNullException">The filename or encoding is null.</exception>
    /// <exception cref="FileNotFoundException">The file cannot be found.</exception>
    /// <exception cref="DirectoryNotFoundException">The specified path is invalid, for example 
    /// on an unmapped drive.</exception>
    /// <exception cref="IOException">An I/O error occurred.</exception>
    /// <exception cref="NotSupportedException">The filename uses an incorrect syntax.</exception>
        public Text(string filename, Encoding encoding) : 
            this(filename, encoding, true, DataLoad.Immediate)
        {
        }

    /// <summary>Creates a Text from a text file. The whole file is read before returning.</summary>
    /// <param name="filename">string value containing the text file path.</param>
    /// <param name="encoding">Encoding value or null for default ASCII.</param>
    /// <param name="removeCR">bool value to indicate whether carriage return characters should be removed.</param>
    /// <exception cref="ArgumentException">The filename is an empty string.</exception>
    /// <exception cref="ArgumentNullException">The filename or encoding is null.</exception>
    /// <exception cref="FileNotFoundException">The file cannot be found.</exception>
    /// <exception cref="DirectoryNotFoundException">The specified path is invalid, for example 
    /// on an unmapped drive.</exception>
    /// <exception cref="IOException">An I/O error occurred.</exception>
    /// <exception cref="NotSupportedException">The filename uses an incorrect syntax.</exception>
        public Text(string filename, Encoding encoding, bool removeCR) :
            this(filename, encoding, removeCR, DataLoad.Immediate)
        {
        }

    /// <summary>Creates a Text from a text file. Carriage returns are removed.</summary>
    /// <param name="filename">string value containing the text file path.</param>
    /// <param name="encoding">Encoding value or null for default ASCII.</param>
    /// <param name="dataLoad">DataLoad value to indicate timing of loading of data from the file.</param>
    /// <exception cref="ArgumentException">The filename is an empty string, or dataLoad is set to
    /// OnDemand but the file cannot be seeked or the encoding is variable width.</exception>
    /// <exception cref="ArgumentNullException">The filename or encoding is null.</exception>
    /// <exception cref="FileNotFoundException">The file cannot be found.</exception>
    /// <exception cref="DirectoryNotFoundException">The specified path is invalid, for example 
    /// on an unmapped drive.</exception>
    /// <exception cref="IOException">An I/O error occurred.</exception>
    /// <exception cref="NotSupportedException">The filename uses an incorrect syntax.</exception>
        public Text(string filename, Encoding encoding, DataLoad dataLoad) :
            this(filename, encoding, true, dataLoad)
        {
        }

    /// <summary>Creates a Text from a text file.</summary>
    /// <param name="filename">string value containing the text file path.</param>
    /// <param name="encoding">Encoding value or null for default ASCII.</param>
    /// <param name="removeCR">bool value to indicate whether carriage return characters should be removed.</param>
    /// <param name="dataLoad">DataLoad value to indicate timing of loading of data from the file.</param>
    /// <exception cref="ArgumentException">The filename is an empty string, or dataLoad is set to
    /// OnDemand but the file cannot be seeked or the encoding is variable width.</exception>
    /// <exception cref="ArgumentNullException">The filename or encoding is null.</exception>
    /// <exception cref="FileNotFoundException">The file cannot be found.</exception>
    /// <exception cref="DirectoryNotFoundException">The specified path is invalid, for example 
    /// on an unmapped drive.</exception>
    /// <exception cref="IOException">An I/O error occurred.</exception>
    /// <exception cref="NotSupportedException">The filename uses an incorrect syntax.</exception>
        public Text(string filename, Encoding encoding, bool removeCR, DataLoad dataLoad)
        {
            _ID = GetNextID();
            _blocks = new TextBlockList(this);
            _filename = filename;
            _encoding = encoding;
            if(_encoding==null) _encoding = Encoding.ASCII;
            _removeCR = removeCR;
            _dataLoad = dataLoad;

            switch (dataLoad)
            {
        // Immediate load - just drag it all in, allowing other reads, making sure we release resources
            case DataLoad.Immediate:
                using (FileStream file = new FileStream(_filename, FileMode.Open, FileAccess.Read, FileShare.Read))
                using (StreamReader stream = new StreamReader(file, _encoding))
                    TextFromStream(stream);
                break;

        // In the background - start the background thread, and wait for the first block to appear
            case DataLoad.Background:
                _backFinished = false;
                _backQuit = false;
                _backThread = new Thread(new ThreadStart(BackgroundReadThreadProc));
                _backThread.IsBackground = true;
                _backThread.Start();

                BackgroundFirst();
                break;

        // On demand - check it is possible, then make up a list of empty blocks
            case DataLoad.OnDemand:
                long fileLen;
                byte[] buffer = new byte[4];

            // We seek in the FileStream, but read from a StreamReader to get decoding
                using (FileStream file = new FileStream(_filename, FileMode.Open, FileAccess.Read, FileShare.Read))
                {
                    if (!file.CanSeek)
                        throw new ArgumentException("The file cannot be seeked.", "filename");
                    fileLen = file.Length;

                // Check the encoding has fixed length so we can work out how many bytes per character (given that
                // we don't support anything outside Unicode Plane 0); we also check for a byte order mark which
                // we will need to account for when we seek, but we don't attempt to detect the correct encoding
                    file.Read(buffer, 0, 4);
                }

                int bytesPerChar;

                if (_encoding.IsSingleByte)
                    bytesPerChar = 1;
                else if (_encoding == Encoding.Unicode)
                {
                    bytesPerChar = 2;
                    if (buffer[0]==0xFF && buffer[1]==0xFE) _demandBOM = bytesPerChar;
                }
                else if (_encoding == Encoding.BigEndianUnicode)
                {
                    bytesPerChar = 2;
                    if (buffer[0]==0xFE && buffer[1]==0xFF) _demandBOM = bytesPerChar;
                }
                else if (_encoding == Encoding.UTF32)
                {
                    bytesPerChar = 4;
                    if (buffer[0]==0xFF && buffer[1]==0xFE && buffer[2]==0 && buffer[3]==0) _demandBOM = bytesPerChar;
                }
                else if (_encoding ==  Encoding.GetEncoding("UTF-32BE"))
                {
                    bytesPerChar = 4;
                    if (buffer[0]==0 && buffer[1]==0 && buffer[2]==0xFE && buffer[3]==0xFF) _demandBOM = bytesPerChar;
                }
                else
                    throw new ArgumentException("Encoding is not fixed width.", "encoding");

            // Set up empty blocks with offsets accordingly; unless we are removing carriage returns, we know how
            // long the text will be when we read it
                if (fileLen > _demandBOM)
                {
                    for (long offset=0; offset<fileLen; offset+=bytesPerChar*tgtBlock)
                    {
                        int blockLen;
                        if (removeCR)
                            blockLen = -1;
                        else
                            blockLen = (int)Math.Min((fileLen-offset)/bytesPerChar, tgtBlock);
                        _blocks.AddLast(new TextBlock(offset, blockLen));
                    }
                }
                else
                    _blocks.AddLast(new TextBlock()); 

            // Unmodified blocks to hold in memory, in a short FIFO queue; note a lot of blocks will stay in the 
            // system cache anyway so this isn't a big deal
                _demandBlocks = new Queue<TextBlock>(_demandMax);

            // If we are not removing carriage returns then we know the whole text length now
                if (removeCR)
                    _readLength = -1;
                else
                    _readLength = (fileLen - _demandBOM) / bytesPerChar;

             // Start a thread to read as needed
                _demandQuit = false;
                _demandFinished = false;
                _demandOffset = -1;
                _demandThread = new Thread(new ThreadStart(OnDemandReadThreadProc));
                _demandThread.IsBackground = true;
                _demandThread.Start();
                break;
            }

            _cursor = new Mark(this, false);
            _marks.Add(_cursor);
            Check();
        }

    /// <summary>Creates a Text from a StreamReader. Carriage return characters are removed, and the whole
    /// stream is read before returning.</summary>
    /// <param name="stream">StreamReader to read text from.</param>
    /// <exception cref="ObjectDisposedException">The StreamReader is closed.</exception>
    /// <exception cref="IOException">An I/O error occurred.</exception>
        public Text(StreamReader stream) : this(stream, true)
        {
        }

    /// <summary>Creates a Text from a StreamReader. The whole stream is read before returning.</summary>
    /// <param name="stream">StreamReader to read text from.</param>
    /// <param name="removeCR">bool value to indicate whether carriage return characters should be removed.</param>
    /// <exception cref="ObjectDisposedException">The StreamReader is closed.</exception>
    /// <exception cref="IOException">An I/O error occurred.</exception>
        public Text(StreamReader stream, bool removeCR)
        {
            _ID = GetNextID();
            _blocks = new TextBlockList(this);
            _filename = "TelOutput.txt";
            _removeCR = removeCR;
            _dataLoad = DataLoad.Immediate;

            TextFromStream(stream);
            _encoding = stream.CurrentEncoding;

            _cursor = new Mark(this, false);
            _marks.Add(_cursor);
            Check();
        }

    //---------------------------------------------------------------------------------------------
    
    // Get the next ID - thread-safe
        private static int GetNextID()
        {
            lock (_idLock)
                return ++_IDs;
        }

    // Fill the Text from a StreamReader immediately
        private void TextFromStream(StreamReader stream)
        {
            char[] buffer = new char[tgtBlock];
            int read;

            do
            {
                read = stream.ReadBlock(buffer, 0, tgtBlock);
                if (read > 0)
                {
                    int clean = read;
                    if (_removeCR) clean = CleanCarriageReturns(buffer, read);
                    if (clean>0) _blocks.AddLast(new TextBlock(buffer,clean));
                    _readLength += clean;
                }
            } while (read > 0);

            if (_blocks._count == 0) _blocks.AddLast(new TextBlock());
        }

    //---------------------------------------------------------------------------------------------
    
    // Thread routine to read the text file in the background, signalling after each read so main
    // thread can check if the block it wants is there
        private void BackgroundReadThreadProc()
        {
#if IOTEST
            Console.WriteLine("BackgroundReadThread: starting");
#endif
            char[] buffer = new char[tgtBlock];

            try
            {
                using (FileStream file = new FileStream(_filename, FileMode.Open, FileAccess.Read, FileShare.Read))
                {
                    using (StreamReader stream = new StreamReader(file, _encoding))
                    {
                        int read;

                        do
                        {
                            read = stream.ReadBlock(buffer, 0, tgtBlock);
#if IOTEST
                            Thread.Sleep(100);
                            Console.WriteLine("BackgroundReadThread: {0} bytes", read);
#endif
                            if (read > 0)
                            {
                                int clean = read;
                                if (_removeCR) clean = CleanCarriageReturns(buffer, read);
                                if (clean>0)
                                    lock (_backLock)
                                    {
                                        if (!_backQuit)
                                        {
                                            _blocks.AddLast(new TextBlock(buffer,clean));
                                            _readLength += clean;
                                        }
                                        Monitor.Pulse(_backLock);
                                    }
                            }
                        } while (read > 0 && !_backQuit);
                    }
                }
            }
            catch (Exception ex)
            {
                _backError = ex;
            }
            finally
            {
                _backFinished = true;
#if IOTEST
                Thread.Sleep(100);
                Console.WriteLine("BackgroundReadThread: ending");
#endif
            }
        }

    // Called by constructor to get the first block before returning
        internal void BackgroundFirst()
        {
#if IOTEST
            Console.WriteLine("BackgroundFirst: starting");
#endif
            lock (_backLock)
            {
                while (!_backFinished && _blocks._first==null)
                {
                    Monitor.Wait(_backLock, 10);
#if IOTEST
                    Console.WriteLine("BackgroundFirst: waited, finished = {0}, first null = {1}", _backFinished, (_blocks._first==null));
#endif
                }
            }

            if (_backError != null) throw _backError;

        // If no (non-CR) text was found, add an empty block because we always need at least one
            if (_blocks._count == 0) _blocks.AddLast(new TextBlock());
#if IOTEST
            Console.WriteLine("BackgroundFirst: ending");
#endif
        }

    // Called by a text block in background mode to wait for the next block to arrive
        internal TextBlock BackgroundNext(TextBlock block) 
        {
#if IOTEST
            Console.WriteLine("BackgroundNext: starting");
#endif
            lock (_backLock)
                while (!_backFinished && block._next==null)
                {
                    Monitor.Wait(_backLock, 10);
#if IOTEST
                    Console.WriteLine("BackgroundNext: waited, finished = {0}, next null = {1}", _backFinished, (block._next==null));
#endif
                }

            if (_backError != null) throw _backError;
#if IOTEST
            Console.WriteLine("BackgroundNext: ending");
#endif
            return block._next;
        }

    // Called in background mode to wait for the whole load to be complete
        internal void BackgroundFinish()
        {
#if IOTEST
            Console.WriteLine("BackgroundFinish: starting, finished = {0}", _backFinished);
#endif
            if (_backFinished) return;

            lock (_backLock)
                while (!_backFinished)
                {
                    Monitor.Wait(_backLock, 100);
#if IOTEST
                    Console.WriteLine("BackgroundFinish: waited, finished = {0}", _backFinished);
#endif
                }

            if (_backError != null) throw _backError;
#if IOTEST
            Console.WriteLine("BackgroundFinish: ending");
#endif
            return;
        }

    // Called in background mode to tell the reader to quit
        internal void BackgroundQuit()
        {
#if IOTEST
            Console.WriteLine("BackgroundQuit: starting");
#endif
            if (_backThread != null && _backThread.ThreadState != System.Threading.ThreadState.Unstarted)
            {
                lock (_backLock)
                    _backQuit = true;
                _backThread.Join();

            }
#if IOTEST
            Console.WriteLine("BackgroundQuit: ending");
#endif
        }

    // Thread routine for loading data in on-demand mode; main thread will trigger a read of the next
    // block as well as the one it currently wants, so we should be able to run in parallel
        private void OnDemandReadThreadProc()
        {
#if IOTEST
            Console.WriteLine("OnDemandReadThread: starting");
#endif
            try
            {
                using (FileStream file = new FileStream(_filename, FileMode.Open, FileAccess.Read, FileShare.Read))
                {
                    using (StreamReader stream = new StreamReader(file, _encoding))
                    {
                        long currentOffset = -1;
                        _demandBuffer = new char[][] { new char[tgtBlock], new char[tgtBlock] };
                        _demandWhich = 0;

                        do
                        {

                        // Wait until we get a request for some action (both offsets are -1 initially)
                            lock (_demandLock)
                            {
                                while (!_demandQuit && _demandOffset == currentOffset)
                                {

                                    Monitor.Wait(_demandLock);
//                                    Monitor.Wait(_demandLock, 100);
#if IOTEST
                                    Console.WriteLine("OnDemandReadThread waited: quit {0} current {1} requested {2}", _demandQuit, currentOffset, _demandOffset);
#endif
                                }

                            // Remember which block we are supposed to read now and where to read it into
                                currentOffset = _demandOffset;
                                _demandWhich = 1 - _demandWhich;
                            }

                        // Die if the Text is being disposed of
                            if (_demandQuit) break;

                        // Fiddle needed to dodge byte order mark on seeking to first block!
                            long realOffset = currentOffset;
                            int readChars = tgtBlock;
                            if (currentOffset == 0)
                            {
                                realOffset = _demandBOM;
                                readChars = readChars - 1;
                            }

                        // Read from the given offset - but there's a good chance we are already there
                            if (realOffset != stream.BaseStream.Position)
                            {
                                stream.DiscardBufferedData();
                                stream.BaseStream.Seek(realOffset, SeekOrigin.Begin);
                            }
                            _demandRead = stream.ReadBlock(_demandBuffer[_demandWhich], 0, readChars);
#if IOTEST
                            Console.WriteLine("OnDemandReadThread: {0} chars at {1}", _demandRead, currentOffset);
#endif

                        // Let the main thread know that we've got a block ready to go - but by now the main thread
                        // may have changed its mind about which block it wants, in which case we just have to go round
                            lock (_demandLock)
                            {
                                if (_demandOffset == currentOffset)
                                {
#if IOTEST
                                    Console.WriteLine("OnDemandReadThread: requested block ready at {0}", currentOffset);
#endif
                                    _demandReady = true;
                                    Monitor.Pulse(_demandLock);
                                }
#if IOTEST
                                else
                                    Console.WriteLine("OnDemandReadThread: unwanted block read at {0}", currentOffset);
#endif
                            }

                        } while (!_demandQuit);
                    }
                }
            }
            catch (Exception ex)
            {
                _demandError = ex;
            }
            finally
            {
#if IOTEST
                Console.WriteLine("OnDemandReadThread: ending");
#endif
                _demandFinished = true;
            }
        }

    // Called by a text block in on-demand mode to read in data for this block and the next when
    // this block has not got its data already
        internal StringBuilder OnDemandRead(TextBlock block)
        {
#if IOTEST
            Console.WriteLine("OnDemandRead: starting");
#endif
            int which; // Choice of buffer filled by the read thread
            int read;  // Number of chars read

            lock (_demandLock)
            {
            // If the read thread is loading or has loaded the wrong block, correct it
                if (block._offset != _demandOffset)
                {
#if IOTEST
                    Console.WriteLine("OnDemandRead: wrong block loaded {0}, want {1}", _demandOffset, block._offset);
#endif
                    _demandReady = false;
                    _demandOffset = block._offset;
                    Monitor.Pulse(_demandLock);
                }
#if IOTEST
                else
                    Console.WriteLine("OnDemandRead: right block being loaded {0}", _demandOffset);
#endif

            // Either way we may have to wait for it to be ready or fail
                while (!_demandReady && !_demandFinished) {
                    Monitor.Wait(_demandLock,100);
#if IOTEST
                    Console.WriteLine("OnDemandRead: waited, ready = {0}", _demandReady);
#endif
                }
                if (_demandError != null) throw _demandError;
#if IOTEST
                Console.WriteLine("OnDemandRead: block loaded at {0}", _demandOffset);
#endif

            // Remember which buffer the read thread used before it changes
                which = _demandWhich;
                read = _demandRead;

            // Now see if we should set it load the next block in the background
                if (block._next != null && block._next._data == null)
                {
                    _demandReady = false;
                    _demandOffset = block._next._offset;
#if IOTEST
                    Console.WriteLine("OnDemandRead: triggering next block at {0}", _demandOffset);
#endif
                    Monitor.Pulse(_demandLock);
                }
            }

        // Clean carriage returns from the buffer if required
            int clean = read;
            if (_removeCR) clean = CleanCarriageReturns(_demandBuffer[which], read);

        // Just couldn't decide how to safely deal with this - we would have to remove this or 
        // neighbouring blocks, but the calling code has references; or we could allow empty blocks
        // but that would mean a lot of changes and be less efficient in the general case
            if (clean==0)
                throw new InvalidDataException("Text block contains only carriage return characters.");

        // Make a StringBuilder to attach to the block and remember its length
            block._data = new StringBuilder(new String(_demandBuffer[which], 0, clean), Text.maxBlock);
            block._length = block._data.Length;

        // Keep just a handful of disposable (unchanged) blocks
            while (_demandBlocks.Count >= _demandMax)
            {
                TextBlock oldBlock = _demandBlocks.Dequeue();
                if (oldBlock._offset >= 0) oldBlock._data = null;
            }
            _demandBlocks.Enqueue(block);

#if IOTEST
            Console.WriteLine("OnDemandRead: ending");
#endif
            return block._data;
        }

    // Called in on-demand mode to tell the reader to quit
        internal void OnDemandQuit()
        {
#if IOTEST
            Console.WriteLine("OnDemandQuit: starting");
#endif
            if (_demandThread != null && _demandThread.ThreadState != System.Threading.ThreadState.Unstarted)
            {
                lock (_demandLock)
                {
                    _demandQuit = true;
                    Monitor.Pulse(_demandLock);
                }
                _demandThread.Join();
            }
#if IOTEST
            Console.WriteLine("OnDemandQuit: ending");
#endif
        }

    // Clean carriage returns from a read buffer
        private int CleanCarriageReturns(char[] buffer, int inLen)
        {
            int outLen = 0;
            for (int i = 0; i<inLen; i++)
                if (buffer[i] != '\r')
                    buffer[outLen++] = buffer[i];
            return outLen;
        }


    //---------------------------------------------------------------------------------------------

    /// <summary>Releases resources used by the Text.</summary>
    // This is required because we now have a potential background thread, and an on-demand stream.
        public void Dispose()
        {
            // Tell any background or on-demand read thread to stop (it will dispose of its own streams)
            if (_dataLoad == DataLoad.Background) BackgroundQuit();
            else if (_dataLoad == DataLoad.OnDemand) OnDemandQuit();

            // Drop referenced objects so the GC can get them even if user doesn't null his references
            _blocks = null;
            _filename = null;
            _encoding = null;
            _demandBlocks = null;

            foreach (Mark mark in _marks)
            {
                mark._block = null;
                mark._text = null;
            }
            _marks = null;

            foreach (Mark mark in _invalMarks)
            {
                mark._block = null;
                mark._text = null;
            }
            _invalMarks = null;

            _boxes = null;
        }
    
    //---------------------------------------------------------------------------------------------
    
    /// <summary>Writes the Text to a text file using the stored filename and encoding, calling 
    /// WriteLine at newline characters by default. If carriage returns were not removed from input 
    /// data, no end-of-line processing is done, and CR/LF sequences are expected within the Text.
    /// </summary>
    /// <exception cref="ArgumentNullException">The filename or encoding is null.</exception>
    /// <exception cref="DirectoryNotFoundException">The specified path is invalid, for example 
    /// on an unmapped drive.</exception>
    /// <exception cref="PathTooLongException">The filename exceeds the system-defined maximum
    /// length.</exception>
    /// <exception cref="UnauthorisedAccessException">Access is denied.</exception>
    /// <exception cref="SecurityException">The caller does not have the required permission.
    /// </exception>
    /// <exception cref="IOException">An I/O error occurred.</exception>
        public void Write()
        {
            Write(_filename, _encoding);
        }

    /// <summary>Writes the Text to a text file using the passed filename and ASCII encoding, calling 
    /// WriteLine at newline characters by default. If carriage returns were not removed from input 
    /// data, no end-of-line processing is done, and CR/LF sequences are expected within the Text.
    /// </summary>
    /// <param name="filename">string value containing the text file path.</param>
    /// <exception cref="ArgumentException">The filename is an empty string or the name of a
    /// system device.</exception>
    /// <exception cref="ArgumentNullException">The filename is null.</exception>
    /// <exception cref="DirectoryNotFoundException">The specified path is invalid, for example 
    /// on an unmapped drive.</exception>
    /// <exception cref="PathTooLongException">The filename exceeds the system-defined maximum
    /// length.</exception>
    /// <exception cref="UnauthorisedAccessException">Access is denied.</exception>
    /// <exception cref="SecurityException">The caller does not have the required permission.
    /// </exception>
    /// <exception cref="IOException">An I/O error occurred.</exception>
        public void Write(string filename)
        {
            Write(filename, null);
        }

    /// <summary>Writes the Text to a text file using the passed filename and encoding, calling 
    /// WriteLine at newline characters by default. If carriage returns were not removed from input 
    /// data, no end-of-line processing is done, and CR/LF sequences are expected within the Text.
    /// </summary>
    /// <param name="filename">string value containing the text file path.</param>
    /// <param name="encoding">Encoding value or null for default ASCII.</param>
    /// <exception cref="ArgumentException">The filename is an empty string or the name of a
    /// system device.</exception>
    /// <exception cref="ArgumentNullException">The filename is null.</exception>
    /// <exception cref="DirectoryNotFoundException">The specified path is invalid, for example 
    /// on an unmapped drive.</exception>
    /// <exception cref="PathTooLongException">The filename exceeds the system-defined maximum
    /// length.</exception>
    /// <exception cref="UnauthorisedAccessException">Access is denied.</exception>
    /// <exception cref="SecurityException">The caller does not have the required permission.
    /// </exception>
    /// <exception cref="IOException">An I/O error occurred.</exception>
        public void Write(string filename, Encoding encoding)
        {
            _filename = filename;
            _encoding = encoding;
            if (_encoding == null) _encoding = Encoding.ASCII;
            using (StreamWriter stream = new StreamWriter(_filename,false,_encoding)) Write(stream);
        }

    /// <summary>Writes the Text using a StreamWriter, calling WriteLine at newline characters by 
    /// default. If carriage returns were not removed from input data, no end-of-line processing 
    /// is done, and CR/LF sequences are expected within the Text.</summary>
    /// <param name="stream">StreamWriter to be written to.</param>
    /// <exception cref="ObjectDisposedException">The StreamWriter is closed.</exception>
    /// <exception cref="IOException">An I/O error occurred.</exception>
        public void Write(StreamWriter stream)
        {
            TextBlock block = _blocks._first;
            while (block != null)
            {
                int start = 0;
                StringBuilder sb = block.GetData();
                if (_removeCR)
                {
                    for (int end = 0; end < sb.Length; end++)
                    {
                        if (sb[end] == '\n')
                        {
                            stream.WriteLine(sb.ToString(start, end - start));
                            start = end + 1;
                        }
                    }
                    stream.Write(sb.ToString(start, sb.Length - start));
                }
                else
                {
                    stream.Write(sb.ToString());
                }
                block = block.GetNext();
            }
        }

    //---------------------------------------------------------------------------------------------
    
    /// <summary><para>Inserts text at the current cursor position in the Text.</para>
    /// <para>The cursor position is moved to the end of the inserted text, and the Text is 
    /// returned to allow chaining of calls. Other Marks originally at the cursor
    /// position are not moved.</para></summary>
    /// <param name="text">string value containing the text for insertion.</param>
        public Text Insert(string text)
        {
            _cursor.Insert(text);
            return this;
        }

    /// <summary><para>Inserts text at the current cursor position in the Text.</para>
    /// <para>The cursor position is moved to the end of the inserted text, and the Text is 
    /// returned to allow chaining of calls. Other Marks originally at the cursor
    /// position are not moved.</para></summary>
    /// <param name="source">object whose ToString method will define the text for insertion.</param>
        public Text Insert(object source)
        {
            _cursor.Insert(source.ToString());
            return this;
        }

    /// <summary><para>Deletes text forwards or backwards from the current cursor position.</para>
    /// <para>The Text is returned to allow chaining of calls.</para></summary>
    /// <param name="chars">Number of characters to delete, negative for deletion backwards,
    /// and positive for deletion forwards.</param>
        public Text Delete(long chars)
        {
            _cursor.Delete(chars);
            return this;
        }

    /// <summary>Deletes text forwards or backwards from the current cursor position, returning
    /// the deleted text as a string.</summary>
    /// <param name="chars">Number of characters to delete, negative for deletion backwards,
    /// and positive for deletion forwards.</param>
        public string Cut(int chars)
        {
            return _cursor.Cut(chars);
        }

    //---------------------------------------------------------------------------------------------

    // Split a block at the given position; we may be at the end of the block in which case an
    // empty block is created, but a subsequent Join will always remove it again
        internal void Split(TextBlock block, int position)
        {
            StringBuilder oldSB = block.GetData();

        // Create new block with the end contents of the old block
            int oldLen = oldSB.Length;
            TextBlock newBlock = new TextBlock(oldSB.ToString(position, oldLen-position));

        // Truncate the old block and mark it changed
            oldSB.Length = position;
            block._offset = -1;

        // Insert the new block into the list
            _blocks.AddAfter(block, newBlock);

        // Look through all Marks in the Text moving those that are in the block after position
            foreach (Mark mark in _marks)
            {
                if (mark._block==block && mark._position>position)
                {
                    mark._block = newBlock;
                    mark._position -= position;
                }
            }

        }

    // Join a block with the next one if it exists and if the lengths add up to no more than max
        internal void Join(TextBlock block)
        {
        // Skip if no next block
            if (block.GetNext()==null) return;
            StringBuilder oldSB = block.GetData();
            StringBuilder newSB = block._next.GetData();

        // Skip if the two blocks are too long
            int oldLength = oldSB.Length;
            if (oldLength + newSB.Length > maxBlock) return;

        // Copy the contents
            oldSB.Append(newSB.ToString());

        // Look through all Marks in the Text moving those that were in the second block
            foreach (Mark mark in _marks)
            {
                if (mark._block==block._next)
                {
                    mark._block = block;
                    mark._position += oldLength;
                }
            }

            _blocks.Remove(block._next);
        }

    // Pull the Marks in a span back to the start of the span (before deleting it) and return
    // the Marks that were right at the end of the span in case we are replacing (unless the
    // span is actually empty)
        internal List<Mark> Pull(Mark mark1, Mark mark2)
        {
        // Look through the blocks in the Range, and find any Marks in those blocks - more
        // efficient than the other way around with a large Text
            TextBlock block = mark1._block;
            int position = mark1._position;
            List<Mark> endMarks = new List<Mark>();

        // Shortcut if the span is empty - saves testing in last block
            if (mark1._block == mark2._block && mark1._position == mark2._position)
                return endMarks;

        // Whole blocks
            while (block != mark2._block)
            {
                foreach (Mark mark in _marks)
                {
                    if (mark._block == block && mark._position > position)
                    {
                        mark._block = mark1._block;
                        mark._position = mark1._position;
                    }
                }

                block = block._next;
                position = 0;
            }

        // Last block, picking up marks at the end of the span
            foreach (Mark mark in _marks)
            {
                if (mark._block == block && mark._position > position &&
                                            mark._position <= mark2._position)
                {
                    if (mark._position == mark2._position)
                        endMarks.Add(mark);
                    mark._block = mark1._block;
                    mark._position = mark1._position;
                }
            }

            return endMarks;
        }

    //---------------------------------------------------------------------------------------------
    
    /// <summary>Creates a new Mark in the Text using the passed Mark value.</summary>
    /// <param name="mark">Mark value representing the initial position of the new Mark.</param>
    /// <exception cref="ArgumentException">The passed Mark has been invalidated or is in another
    /// Text.</exception>
        public Mark AddMark(Mark mark)
        {
            mark.Validate(this, "mark");

            Mark mark2 = new Mark(mark, false); // Not invalidatable because registered
            mark2._ID = Mark.GetNextID();
            _marks.Add(mark2);
            return mark2;
        }

    /// <summary>Creates a new Mark in the Text at the current cursor position.</summary>
        public Mark AddMark()
        {
            return AddMark(_cursor);
        }

    // Add a new mark to the invalidatable list, so that it will be invalidated as the text 
    // changes; but if the list is full, invalidate the first one now and lose it.
        internal void Invalidatable(Mark mark)
        {
            while (_invalMarks.Count >= maxInval)
                _invalMarks.Dequeue()._ID = -1;
            _invalMarks.Enqueue(mark);
            mark._ID = 0;
        }

    // Called when the text changes to invalidate all the unregistered marks.
        internal void InvalidateAll()
        {
            foreach (Mark mark in _invalMarks) mark._ID = -1;
            _invalMarks.Clear();
        }

    //---------------------------------------------------------------------------------------------
    
    /// <summary>Creates a new Range in the Text using start and end Mark values.</summary>
    /// <param name="mark1">Mark value representing the start of the new Range.</param>
    /// <param name="mark2">Mark value representing the end of the new Range.</param>
    /// <exception cref="ArgumentException">One of the passed Marks has been invalidated or is in
    /// another Text, or the Marks are in the wrong order.</exception>
        public Range AddRange(Mark mark1, Mark mark2)
        {
            mark1.Validate(this, "mark1");
            mark2.Validate(this, "mark2");
            if (mark2 - mark1 < 0)
                throw new ArgumentException("Mark arguments are in the wrong order.",
                                                                                  "mark1, mark2");

            Mark mark1New = AddMark(mark1);
            Mark mark2New = AddMark(mark2);

            Range range = new Range(mark1New, mark2New);
            range._ID = Range.GetNextID();
            return range;
        }

    /// <summary>Creates a new Range in the Text using the passed Range value.</summary>
    /// <param name="range">Range value representing the initial span of the new Range.</param>
    /// <exception cref="ArgumentException">The passed Range has been invalidated or is in another
    /// Text.</exception>
        public Range AddRange(Range range)
        {
            range.Validate(this, "range");
            return AddRange(range._mark1, range._mark2);
        }

    /// <summary>Creates a new Range in the Text using an offset from the current cursor position.
    /// </summary>
    /// <param name="offset"><para>Offset (positive or negative) from the current cursor 
    /// position.</para><para>The Range stretches between the current cursor position and the
    /// offset position.</para></param>
        public Range AddRange(long offset)
        {
            if(offset>=0)
            {
                return AddRange(_cursor, _cursor.OffsetMark(offset,false));
            } else {
                return AddRange(_cursor.OffsetMark(offset,false), _cursor);
            }
        }

    /// <summary>Creates an empty new Range at the start of the Text.</summary>
        public Range AddRange()
        {
            return AddRange(StartMark(false), StartMark(false));
        }

    //---------------------------------------------------------------------------------------------
    
    /// <summary><para>Removes a Mark from the list of Marks registered with the Text.</para>
    /// <para>The Mark is invalidated. The Text is returned to allow chaining of calls.</para>
    /// </summary>
    /// <param name="mark">Mark to be removed from the Text.</param>
    /// <exception cref="ArgumentException">The passed Mark is in another Text.</exception>
        public Text Remove(Mark mark)
        {
            if (this != mark._text)
                throw new ArgumentException("Mark argument is in a different Text.", "range");

            mark._ID = -1;
            try
            {
                _marks.Remove(mark);
            }
            catch   // We don't care if the Mark is removed twice or was never registered
            {
            }
            return this;
        }

    /// <summary><para>Removes a Range from the list of Ranges registered with the Text.</para>
    /// <para>The Range is invalidated. The Text is returned to allow chaining of calls.</para>
    /// </summary>
    /// <param name="range">Range to be removed from the Text.</param>
    /// <exception cref="ArgumentException">The passed Range is in another Text.</exception>
        public Text Remove(Range range)
        {
            if (this != range._mark1._text)
                throw new ArgumentException("Range argument is in a different Text.", "range");

            range._ID = -1;
            try
            {
                range._mark1._ID = -1;
                _marks.Remove(range._mark1);
            }
            catch
            {
            }
            try
            {
                range._mark2._ID = -1;
                _marks.Remove(range._mark2);
            }
            catch
            {
            }
            return this;
        }

    //---------------------------------------------------------------------------------------------
    
    /// <summary><para>Moves the current cursor position forwards or backwards. No exception is
    /// raised if the start or end of the text is reached.</para>
    /// <para>The Text is returned to allow chaining of calls.</para></summary>
    /// <param name="offset">Number of characters to move, positive or negative.</param>
        public Text MoveTo(long offset)
        {
            _cursor.MoveTo(offset);
            return this;
        }

    /// <summary><para>Moves the current cursor position to a passed Mark position.</para>
    /// <para>The Text is returned to allow chaining of calls.</para></summary>
    /// <param name="mark">Mark position to move to.</param>
    /// <exception cref="ArgumentException">The passed Mark has been invalidated or is in another
    /// Text.</exception>
        public Text MoveTo(Mark mark)
        {
            mark.Validate(this, "mark");
            _cursor.MoveTo(mark);
            return this;
        }

    /// <summary>Moves the current cursor position to the beginning of the next line if there is 
    /// one, and returns true, or leaves the cursor unmoved and returns false.</summary>
        public bool NextLine()
        {
            Mark end = _cursor.LineEnd(false);
            if (end._block==_blocks._last && end._position==_blocks._last.GetLength()) return false;
            MoveTo(end.OffsetMark(1,false));
            return true;
        }

    /// <summary>Moves the current cursor position to the beginning of the previous line if there
    /// is one, and returns true, or leaves the cursor unmoved and returns false.</summary>
        public bool PreviousLine()
        {
            Mark start = _cursor.LineStart(false);
            if (start._block==_blocks._first && start._position==0) return false;
            MoveTo(start.OffsetMark(-1,false));
            MoveTo(_cursor.LineStart(false));
            return true;
        }

    //---------------------------------------------------------------------------------------------
    
    /// <summary>Converts the Text contents to a string with newline characters at line ends.</summary>
        public override string ToString()
        {
            if (_dataLoad == DataLoad.Background) BackgroundFinish();  // Quicker to get it in one go

            StringBuilder text = new StringBuilder();
            TextBlock block = _blocks._first;
            while (block != null)
            {
                text.Append(block.GetData());
                block = block._next;
            }
            return text.ToString();
        }

    /// <summary><para>Returns a Mark at the start of the current line in the Text.</para>
    /// <para>Do not assign this value to a Mark variable - it will become invalidated if the
    /// text changes.  Use Text.AddMark() or Mark.MoveTo() instead.</para></summary>
        public Mark LineStart()
        {
            return _cursor.LineStart();
        }

    /// <summary><para>Returns a Mark at the end of the current line in the Text.</para>
    /// <para>Do not assign this value to a Mark variable - it will become invalidated if the
    /// text changes.  Use Text.AddMark() or Mark.MoveTo() instead.</para></summary>
        public Mark LineEnd()
        {
            return _cursor.LineEnd();
        }

    /// <summary><para>Returns a Range containing the line (excluding line feeds) which the current
    /// cursor is in.</para>
    /// <para>Do not assign this value to a Range variable - it will become invalidated if the 
    /// text changes.  Use Text.AddRange() or Range.MoveTo() instead.</para></summary>
        public Range Line()
        {
            return new Range(_cursor.LineStart(), _cursor.LineEnd());
        }

    //---------------------------------------------------------------------------------------------
    
    /// <summary>Char at the given index in the Text.</summary>
    /// <param name="index">Offset from the start of the Text.</param>
    /// <exception cref="ArgumentOutOfRangeException">The offset is negative or greater than the
    /// length of the Text.</exception>
    // Not using All[index] now because of efficiency of Length(), extra marks needed.
        public char this[long index]
        {
            get
            {
                if (index<0)
                    throw new ArgumentOutOfRangeException("index", "Index may not be negative.");
                Mark mark = StartMark(false).OffsetMark(index, false);

            // Look out - if we are at the end of the block we have to go to the next one; we are
            // relying on no empty blocks unless the Text is empty; no next block means out of range
                StringBuilder sb = mark._block.GetData();
                if (mark._position==sb.Length)
                {
                    if (mark._block.GetNext() == null)
                        throw new ArgumentOutOfRangeException("index", "Index must be less than Length.");
                    return mark._block._next.GetData()[0];
                }
                else return sb[mark._position];
            }

            set
            {
                if (index<0)
                    throw new ArgumentOutOfRangeException("index", "Index may not be negative.");
                Mark mark = StartMark(false).OffsetMark(index, false);

            // Look out - if we are at the end of the block we have to go to the next one; we are
            // relying on no empty blocks unless the Text is empty; no next block means out of range
                StringBuilder sb = mark._block.GetData();
                if (mark._position==sb.Length)
                {
                    if (mark._block.GetNext() == null)
                        throw new ArgumentOutOfRangeException("index", "Index must be less than Length.");
                    mark._block._next.GetData()[0] = value;
                }
                else sb[mark._position] = value;

            // See if we have made any Boxes dirty - assume not many, all short
                foreach (Box box in _boxes)
                {
                    if (!box.DirtyRange && mark.Within(box.Range)) box.OnRangeDirty();
                }
            }
        }

    /// <summary>Length of the text in the Text including newline characters.  If data is loaded from a
    /// file in the background or on demand with carriage returns removed, the whole file will be read
    /// before a value is returned.</summary>
        public long Length
        {
            get
            {
                switch (_dataLoad)
                {
                    case DataLoad.Immediate:
                        return _readLength + _editLength;
                    case DataLoad.Background:
                        BackgroundFinish();
                        return _readLength + _editLength;
                    case DataLoad.OnDemand:
                        if (_readLength < 0)
                        {
                            _readLength = 0;
                            _editLength = 0;
                            TextBlock block = _blocks._first;
                            do
                            {
                                _readLength += block.GetLength();
                                block = block._next;
                            } while (block != null);
                        }
                        return _readLength + _editLength;
                    default:
                        return -1;
                }
            }
        }

    /// <summary><para>Current cursor position in the Text.</para>
    /// <para>Do not assign this value to a Mark variable - it will become invalidated if the
    /// text changes.  Use Text.AddMark() or Mark.MoveTo() instead.</para></summary>
        public Mark Cursor
        {
            get { return new Mark(_cursor, true); }
        }
    
    /// <summary><para>Position at the start of the Text.</para>
    /// <para>Do not assign this value to a Mark variable - it will become invalidated if the
    /// text changes.  Use Text.AddMark() or Mark.MoveTo() instead.</para></summary>
        public Mark Start
        {
            get { return StartMark(true); }
        }

    // Mark at the start of the text - with option for Mark to be invalidatable.
        internal Mark StartMark(bool invalidatable)
        {
            return new Mark(this, invalidatable);
        }

    /// <summary><para>Position at the end of the Text.</para>
    /// <para>Do not assign this value to a Mark variable - it will become invalidated if the
    /// text changes.  Use Text.AddMark() or Mark.MoveTo() instead.</para></summary>
        public Mark End
        {
            get { return EndMark(true); }
        }

    // Mark at the end of the text - with option for Mark to be invalidatable.
        internal Mark EndMark(bool invalidatable)
        {
            if (_dataLoad==DataLoad.Background) BackgroundFinish();
            return new Mark(this, _blocks._last, _blocks._last.GetLength(), invalidatable);
        }

    /// <summary><para>Range containing the whole contents of the Text.</para>
    /// <para>Do not assign this value to a Range variable - it will become invalidated if the
    /// text changes.  Use Text.AddRange() or Range.MoveTo() instead.</para>
    /// </summary>
        public Range All
        {
            get { return new Range(StartMark(true), EndMark(true)); }
        }

    /// <summary>Override for default GetHashCode() - returns a unique index.</summary>
        public override int GetHashCode()
        {
            return _ID;
        }

    // Add a Box to the list of boxes whose ranges will be tracked
        internal void AddBox(Box box)
        {
            _boxes.Add(box);
        }

    // Remove a Box from the list of boxes
        internal void Remove(Box box)
        {
            try
            {
                _boxes.Remove(box);
            }
            catch { }
        }

    // Debug - called when the text has changed to check we haven't fouled up; I think we can
    // assume this will be inlined/eliminated when DEBUG is not defined
        internal void Check()
        {
#if DEBUG
        // This will not work well with ongoing background data load, and will mess up on-demand tests
            if (_dataLoad == DataLoad.Immediate ||
                _dataLoad == DataLoad.Background && _backFinished)
            {

                // Check there is at least one block
                Debug.Assert(_blocks._first != null);

                // Check we have no empty blocks unless there is only one block
                if (_blocks._count>1)
                {
                    TextBlock block = _blocks._first;
                    while (block != null)
                    {
                        Debug.Assert(block.GetLength()>0);
                        block = block._next;
                    }
                }

                // Check that no marks are at the start of any block except the first
                TextBlock first = _blocks._first;
                foreach (Mark mark in _marks)
                    Debug.Assert(mark._position>0 || mark._block==first);
            }
#endif
        }
    }

// ================================================================================================

/// <summary>Specifies the timing of data loading from a file when the Text is created.</summary>
    public enum DataLoad
    {
    /// <summary>Loads all data before returning from the Text constructor (default).</summary>
        Immediate,
    /// <summary>Returns immediately then loads all data in a background thread.</summary>
        Background,
    /// <summary>Returns immediately then loads data block-by-block as it is required. Requires
    /// the input stream to be seekable, with fixed-length encoding (UTF-16 is considered fixed
    /// length).</summary>
        OnDemand
    }

// ================================================================================================

// A list of text blocks - replaces LinkedList<TextBlock> to take out a layer of referencing; also
// no error checking because we always know where we are
    internal class TextBlockList
    {
        internal TextBlock _first = null;   // First text block in the list
        internal TextBlock _last = null;    // Last text block in the list
        internal int _count = 0;            // Number of blocks
        internal Text _text;                // Text that the list belongs to

        internal TextBlockList(Text text)
        {
            _text = text;
        }

    // Add a new block at the end - called only by Text constructor so safe
        internal void AddLast(TextBlock block)
        {
            if (_last != null)
            {
                _last._next = block; 
                block._previous = _last;
            }
            else
            {
                _first = block;
            }
            _last = block;
            _count++;
            block._text = this._text;
        }

    // Add a new block in the middle - make have to wait for the next block
        internal void AddAfter(TextBlock preBlock, TextBlock newBlock)
        {
            TextBlock postBlock = preBlock.GetNext();
            newBlock._previous = preBlock;
            newBlock._next = postBlock;
            preBlock._next = newBlock;

            if (postBlock != null)
                postBlock._previous = newBlock;
            else
                _last = newBlock;

            _count++;
            newBlock._text = this._text;
        }

    // Remove a block - may have to wait for the next block
        internal void Remove(TextBlock block)
        {
            TextBlock preBlock = block._previous;
            TextBlock postBlock = block.GetNext();

            if (preBlock != null)
                preBlock._next = postBlock;
            else
                _first = postBlock;

            if (postBlock != null)
                postBlock._previous = preBlock;
            else
                _last = preBlock;

        // Garbage collector doesn't seem able to cope if this block is left pointing into list when
        // compiled with debug or run within Visual Studio - so clean up to help it along
            block._next = null;
            block._previous = null;
            block._data = null;

            _count--;
        }
    }

// ================================================================================================

// A block in the linked list which makes up a Text; sometimes the text will be absent because
// it hasn't been loaded yet; sometimes the list will not be complete
    internal class TextBlock
    {
        internal StringBuilder _data;   // Text or null pointer
        internal long _offset;          // Original position in source file/stream, -1 if we don't care/changed
        internal int _length;           // Length but only used in on-demand mode when block not loaded
        internal Text _text;            // Text to which this block belongs - set when block is added to list
        internal TextBlock _next;       // Next block in linked list or null at end
        internal TextBlock _previous;   // Previous block in linked list or null at start

    // Constructors
        internal TextBlock()
        {
            _data = new StringBuilder(Text.maxBlock);
            _offset = -1;
            _length = 0;
        }

        internal TextBlock(long offset, int length)
        {
            _data = null;
            _offset = offset;
            _length = length;
        }

        internal TextBlock(String data)
        {
            _data = new StringBuilder(data,Text.maxBlock);
            _offset = -1;
            _length = -1;
        }

        internal TextBlock(char[] buffer, int len)
        {
            _data = new StringBuilder(new String(buffer, 0, len), Text.maxBlock);
            _offset = -1;
            _length = -1;
        }

    // Get the next block which may not be there yet in background load mode; note that the
    // previous block always is (so we can always use _previous); so is any block which is
    // at or before any currently referenced block e.g. by a Mark
        internal TextBlock GetNext()
        {
        // If in Background load mode, we may have to wait
            if (_text._dataLoad == DataLoad.Background && !_text._backFinished)
                return _text.BackgroundNext(this);

        // Otherwise the block is always there
            else
                return _next;
        }

    // Get the data from this block ... which may not be there in on-demand mode
        internal StringBuilder GetData()
        {
        // OnDemand means all the blocks are there but not necessarily filled
            if (_text._dataLoad == DataLoad.OnDemand)
            {
                if (_data != null) return _data;
                _data = _text.OnDemandRead(this);
                _length = _data.Length;
            }

        // It is always there with Immediate or Background
            return _data;
        }

    // Get the length of the block - may be remembered in on-demand mode
        internal int GetLength()
        {
            if (_data != null)
                return _data.Length;
            else if (_length >= 0)
                return _length;
            else
            {
                return GetData().Length;
            }
        }

    }

}
